/*
 * Copyright 2010 Gal Dolber.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.unnison.framework.client.place;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.inject.client.AsyncProvider;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ClosingEvent;
import com.google.gwt.user.client.Window.ClosingHandler;
import com.google.inject.Inject;
import com.google.inject.Provider;

import com.unnison.framework.client.async.AbstractAsyncCallback;
import com.unnison.framework.client.jsorm.TypeJsonSerializer;
import com.unnison.framework.client.logging.Logger;

import java.util.HashMap;

public class PlaceManagerImpl extends PlaceManagerBase implements ValueChangeHandler<String>, ClosingHandler {

    private static interface PlaceCallback<D> {
        void onSuccess(Place<D> place);
    }

    private static final String PLACENAMESEPARATOR = "|";

    protected Place<?> currentPlace;
    protected String currentPlaceName;
    protected Object currentData;

    protected String defaultPlace;
    
    private final Crypter crypter;

    protected final HashMap<Class<?>, String> placesNames = new HashMap<Class<?>, String>();

    protected final HashMap<String, Place<?>> places = new HashMap<String, Place<?>>();
    protected final HashMap<String, Provider<? extends Place<?>>> providedPlaces = new HashMap<String, Provider<? extends Place<?>>>();
    protected final HashMap<String, AsyncProvider<? extends Place<?>>> asyncProvidedPlaces = new HashMap<String, AsyncProvider<? extends Place<?>>>();

    protected final HashMap<String, TypeJsonSerializer<?>> serializers = new HashMap<String, TypeJsonSerializer<?>>();
    protected final HashMap<String, Boolean> serializersEncrypted = new HashMap<String, Boolean>();

    private final Logger logger;

    @Inject
    public PlaceManagerImpl(final PlaceManagerInitializer initializer, Logger logger, Crypter crypter) {
        if (!GWT.isScript()) {
            this.logger = logger;
        } else {
            this.logger = null;
        }

        this.crypter = crypter;
        
        initializer.initialize(this);
    }

    public <P extends Place<D>, D> void addPlace(Class<P> clazz, String token, AsyncProvider<P> placeProvider,
        TypeJsonSerializer<D> dataSerializer, boolean encrypted) {
        placesNames.put(clazz, token);
        serializersEncrypted.put(token, encrypted);
        serializers.put(token, dataSerializer);
        asyncProvidedPlaces.put(token, placeProvider);
    }

    public <P extends Place<D>, D> void addPlace(Class<P> clazz, String token, Provider<P> placeProvider,
        TypeJsonSerializer<D> dataSerializer, boolean encrypted) {
        placesNames.put(clazz, token);
        serializersEncrypted.put(token, encrypted);
        serializers.put(token, dataSerializer);
        providedPlaces.put(token, placeProvider);
    }

    @Override
    public String getCurrentToken() {
        return History.getToken();
    }

    @SuppressWarnings("unchecked")
    private <D> void getPlace(final String placeName, final PlaceCallback<D> callback) {
        if (places.containsKey(placeName)) {
            callback.onSuccess((Place<D>) places.get(placeName));
        } else if (providedPlaces.containsKey(placeName)) {
            Provider<? extends Place<?>> placeProvider = providedPlaces.get(placeName);
            Place<?> place = placeProvider.get();
            places.put(placeName, place);
            callback.onSuccess((Place<D>) place);
        } else if (asyncProvidedPlaces.containsKey(placeName)) {
            AsyncProvider<Place<D>> asyncProvider = (AsyncProvider<Place<D>>) asyncProvidedPlaces.get(placeName);
            asyncProvider.get(new AbstractAsyncCallback<Place<D>>() {
                @Override
                public void success(Place<D> place) {
                    places.put(placeName, place);
                    callback.onSuccess(place);
                }
            });
        } else {
            // The exception is only for development mode
            assert false : "Error on history manager. The place " + placeName + " is not registered. It should be binded as Singleton.";

            // In production we just go to the default place
            if (defaultPlace != null) {
                getPlace(defaultPlace, callback);
            }
        }
    }

    @Override
    public <D> String getToken(Class<? extends Place<D>> placeClass) {
        return getToken(placeClass, null);
    }

    @Override
    public <D> String getToken(Class<? extends Place<D>> placeClass, D placeData) {
        String placeName = placesNames.get(placeClass);
        assert placeName != null : "Error on history manager. The place " + placeClass.getName()
            + " is not registered. It should be binded as Singleton.";
        return getToken(placeName, placeData);
    }

    @SuppressWarnings("unchecked")
    protected <D> String getToken(String placeName, D placeData) {
        TypeJsonSerializer<D> s = (TypeJsonSerializer<D>) serializers.get(placeName);
        boolean encrypted = serializersEncrypted.get(placeName);
        if (s != null) {
            if (placeData != null) {
                String json = s.serialize(placeData).toString();
                return "!" + placeName + PLACENAMESEPARATOR + (encrypted ? crypter.encode(json) : json);
            }
        } else {
            assert false : "Error on history manager. The place " + placeName + " is not registered. It should be binded as Singleton.";
        }
        return "!" + placeName;
    }

    @Override
    public <D> void go(Class<? extends Place<D>> placeClass) {
        go(placeClass, null);
        super.go(placeClass);
    }

    @Override
    public <D> void go(Class<? extends Place<D>> placeClass, final D placeData) {
        if (!GWT.isScript()) {
            logger.log("PLACE go " + getToken(placeClass, placeData));
        }
        History.newItem(getToken(placeClass, placeData), false);
        goToPlace(placesNames.get(placeClass), placeData);
        super.go(placeClass, placeData);
    }

    @Override
    public void go(String token) {
        History.newItem(token);
        super.go(token);
    }

    @Override
    public void goBack() {
        History.back();
        super.goBack();
    }

    @Override
    public void goForward() {
        History.forward();
        super.goForward();
    }

    @Override
    public void goDefault() {
        if (defaultPlace != null) {
            History.newItem(getToken(defaultPlace, null), false);
            goToDefaultPlace();
            super.goDefault();
        }
    }

    private void goToDefaultPlace() {
        if (defaultPlace != null) {
            goToPlace(defaultPlace, null);
        }
    }

    private <D> void goToPlace(final String placeName, final D placeData) {
        if (currentPlace != null && currentPlace instanceof StayPlace) {
            StayPlace<?> stayPlace = (StayPlace<?>) currentPlace;
            String warning = stayPlace.mayLeave();
            if (warning != null) {
                if (!Window.confirm(warning)) {
                    // Restore the token
                    History.newItem(getToken(currentPlaceName, currentData), false);

                    stayPlace.stay();
                    return;
                } else {
                    stayPlace.leave();
                }
            }
        }
        getPlace(placeName, new PlaceCallback<D>() {
            @Override
            public void onSuccess(Place<D> place) {
                currentPlace = place;
                currentPlaceName = placeName;
                currentData = placeData;
                place.go(placeData);
            }
        });
    }

    @Override
    public <D> void newItem(Class<? extends Place<D>> placeClass) {
        newItem(placeClass, null);
        super.newItem(placeClass);
    }

    @Override
    public <D> void newItem(Class<? extends Place<D>> placeClass, D placeData) {
        assert placesNames.containsKey(placeClass) : "The place " + placeClass.getName()
            + " is not registered. It should be binded as Singleton.";
        assert placesNames.get(placeClass).equals(currentPlaceName) : "You only can call newItem() for the current place. Otherwise call go().";
        if (!GWT.isScript()) {
            logger.log("PLACE newItem " + getToken(placeClass, placeData));
        }
        History.newItem(getToken(placeClass, placeData), false);
        super.newItem(placeClass, placeData);
    }

    @Override
    public void newItem(String token) {
        History.newItem(token, false);
        super.newItem(token);
    }

    /**
     * Simulate browser token change event (public for testing reasons).
     */
    public void onTokenChange(String token) {
        if (!GWT.isScript()) {
            logger.log("PLACE tokenChange " + token);
        }

        if (token.isEmpty()) {
            goToDefaultPlace();
            return;
        }

        if (!token.startsWith("!")) {
            assert false : "Error on place manager. Found: " + token;

            goToDefaultPlace();
            return;
        }

        int nameSeparator = token.indexOf(PLACENAMESEPARATOR);
        if (nameSeparator != -1) {
            String placeName = token.substring(1, nameSeparator);
            String json = token.substring(nameSeparator + 1);

            Object data = null;
            if (!json.isEmpty()) {
                TypeJsonSerializer<?> typeJsonSerializer = serializers.get(placeName);
                boolean encrypted = serializersEncrypted.get(placeName);
                if (typeJsonSerializer == null) {
                    // The exception is only for development mode
                    assert false : "Error on history manager. The place " + placeName
                        + " is not registered. It should be binded as Singleton.";

                    goToDefaultPlace();
                }
                try {
                    data = typeJsonSerializer.deserialize(JSONParser.parse((encrypted ? crypter.decode(json) : json)));
                } catch (Exception e) {
                    if (GWT.isScript()) {
                        goToDefaultPlace();
                        return;
                    } else {
                        throw new IllegalStateException("Malformed place data", e);
                    }
                }
            }
            goToPlace(placeName, data);
        } else {
            goToPlace(token.substring(1), null);
        }
    }

    /**
     * Token changed handler.
     */
    @Override
    public void onValueChange(ValueChangeEvent<String> event) {
        onTokenChange(event.getValue());
    }

    @Override
    public void onWindowClosing(ClosingEvent event) {
        if (currentPlace != null && currentPlace instanceof StayPlace) {
            String warning = ((StayPlace<?>) currentPlace).mayLeave();
            if (warning != null) {
                event.setMessage(warning);
            }
        }
    }

    /**
     * Set default place.
     */
    protected void setDefaultPlace(String defaultPlace) {
        assert providedPlaces.containsKey(defaultPlace) || asyncProvidedPlaces.containsKey(defaultPlace) : "The default place must implement "
            + Place.class.getName() + ". Found: " + defaultPlace;
        this.defaultPlace = defaultPlace;
    }

    @Override
    public String toString() {
        return placesNames.toString();
    }
}
